﻿/*
* SHARPUPNP
* 
* Written by Marco Minerva, mailto:marco.minerva@gmail.com
* 
* This code is released under the GNU General Public License (GPL) v2.
* A copy of this license is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Xml;

namespace SharpUpnp
{
    /// <summary>
    /// Specifies the protocol of the port mapping.
    /// </summary>
    /// <seealso cref="UpnpNatDevice.AddPortMapping(int, Protocol, out UpnpResponse)"/>
    /// <seealso cref="UpnpNatDevice.DeletePortMapping"/>
    public enum Protocol
    {
        /// <summary>
        /// UDP port mapping protocol.
        /// </summary>
        UDP,
        /// <summary>
        /// TCP port mapping protocol.
        /// </summary>
        TCP
    }    

    /// <summary>
    /// Provides a set of methods for handling the NAT features of an UPnP compliant device. Every methods sends a SOAP message to the device to perform the corresponding action.
    /// </summary>
    public class UpnpNatDevice : UpnpDevice
    {
        private enum Operation
        { 
            AddPortMapping,
            DeletePortMapping,
            GetExternalIPAddress,
            //GetGenericPortMappingEntry,
            //GetPortMappingNumberOfEntries
        }

        /*
         * {0} = ControlURL
         * {1} = ServiceType
         * {2} = Operation (AddPortMapping, RemovePortMapping, ecc.)
         * {3} = IP Remote Address
         * {4} = Remote port
         * {5} = Content Length (in bytes)
         */
        private const string HEADER1 = "POST {0} HTTP/1.1\r\n" +
                                        "SOAPAction: \"{1}#{2}\"\r\n" +
                                        "Content-Type: text/xml; charset=\"utf-8\"\r\n" +
                                        "User-Agent: SharpUPnP (UPnP/1.0)\r\n" +
                                        "Host: {3}:{4}\r\n" +
                                        "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n" +
                                        "Connection: keep-alive\r\n" +
                                        "Content-Length: {5}\r\n" +
                                        "\r\n";
        /*
         * {0} = ControlURL
         * {1} = ServiceType
         * {2} = Operation (AddPortMapping, RemovePortMapping, ecc.)
         * {3} = IP Remote Address
         * {4} = Remote port
         * {5} = Content Length (in bytes)
         */
        private const string HEADER2 = "M-POST {0} HTTP/1.1\r\n" +
                                        "Content-Type: text/xml; charset=\"utf-8\"\r\n" +
                                        "MAN: \"http://schemas.xmlsoap.org/soap/envelope/\"; ns=01\r\n" +
                                        "01-SOAPAction: \"{1}#{2}\"\r\n" +
                                        "User-Agent: SharpUPnP (UPnP/1.0)\r\n" +
                                        "Host: {3}:{4}\r\n" +
                                        "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n" +
                                        "Connection: keep-alive\r\n" +
                                        "Content-Length: {5}\r\n" +
                                        "\r\n";
        /*
         * {0} = ServiceType
         * {1} = IP Internal client   
         * {2} = Port mapping to add
         * {3} = Protocol
         * {4} = Port mapping description
         */
        private const string ADD_PORT_MAPPING_SOAP_MESSAGE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                                                            "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
                                                            "<s:Body>" +
                                                            "<u:AddPortMapping xmlns:u=\"{0}\">" +
                                                            "<NewRemoteHost></NewRemoteHost>" +
                                                            "<NewExternalPort>{2}</NewExternalPort>" +
                                                            "<NewProtocol>{3}</NewProtocol>" +
                                                            "<NewInternalPort>{2}</NewInternalPort>" +
                                                            "<NewInternalClient>{1}</NewInternalClient>" +
                                                            "<NewEnabled>1</NewEnabled>" +
                                                            "<NewPortMappingDescription>{4}</NewPortMappingDescription>" +
                                                            "<NewLeaseDuration>0</NewLeaseDuration>" +
                                                            "</u:AddPortMapping>" +
                                                            "</s:Body>" +
                                                            "</s:Envelope>";
        /*
         * {0} = ServiceType
         */
        private const string GET_EXTERNAL_IP_ADDRESS_SOAP_MESSAGE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                                                                    "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
                                                                    "<s:Body>" +
                                                                    "<u:GetExternalIPAddress xmlns:u=\"{0}\">" +
                                                                    "</u:GetExternalIPAddress>" +
                                                                    "</s:Body>" +
                                                                    "</s:Envelope>";

        /*
         * {0} = ServiceType
         * {1} = Port mapping to remove
         * {2} = Protocol
        */
        private const string REMOVE_PORT_MAPPING_SOAP_MESSAGE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                                                                    "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
                                                                    "<s:Body>" +
                                                                    "<u:DeletePortMapping xmlns:u=\"{0}\">" +
                                                                    "<NewRemoteHost></NewRemoteHost>" +
                                                                    "<NewExternalPort>{1}</NewExternalPort>" +
                                                                    "<NewProtocol>{2}</NewProtocol>" +
                                                                    "</u:DeletePortMapping>" +
                                                                    "</s:Body>" +
                                                                    "</s:Envelope>";

        /*
         * {0} = ServiceType
         */
        //public const string GET_PORT_MAPPING_SOAP_MESSAGE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
        //                                                    "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
        //                                                    "<s:Body>" +
        //                                                    "<u:GetGenericPortMappingEntry xmlns:u=\"{0}\">" +
        //                                                    "<NewPortMappingIndex>0</NewPortMappingIndex>" +
        //                                                    "</u:GetGenericPortMappingEntry>" +
        //                                                    "</s:Body>" +
        //                                                    "</s:Envelope>";

        private const string ERROR_NAMESPACE = "urn:schemas-upnp-org:control-1-0";

        private static IPAddress localAddress;
        /// <summary>
        /// Returns the local (private) IP address of the current system.
        /// </summary>
        public IPAddress LocalAddress
        {
            get { return localAddress; }
        }

        static UpnpNatDevice()
        {
            //Determina l'indirizzo IP della macchina locale.
            foreach (IPAddress address in Dns.GetHostAddresses(Dns.GetHostName()))
            {
                if (address.AddressFamily == AddressFamily.InterNetwork)
                {
                    localAddress = address;
                    break;
                }
            }
        }

        internal UpnpNatDevice(IPEndPoint endPoint) : base(endPoint)
        { }

        #region AddPortMapping

        /// <summary>
        /// Defines a new port mapping on the NAT device for the IP address of the computer, using the specified port and protocol.
        /// </summary>
        /// <param name="port">The port number for the mapping. It must be a value between 1 and 65535.</param>
        /// <param name="protocol">A member of the <strong>Protocol</strong> enumeration that specifies the Internet protocol to be used for the mapping. It can be TCP or UDP.</param>
        /// <param name="response">An <strong>UpnpResponse</strong> variable that will be filled with information about the response of the operation, if any.</param>
        /// <returns><strong>true</strong> if the port mapping definition succeeds, <strong>false</strong> otherwise.</returns>
        /// <exception cref="System.ArgumentException"><em>port</em> is equal or less than 0.</exception>
        /// <exception cref="System.InvalidOperationException">The format of the response message is incorrect.</exception>
        /// <remarks>This method sends an <strong>AddPortMapping</strong> SOAP message to the NAT device. If the methods fails, the <strong>response</strong> object will contain the error code and description.</remarks>
        /// <seealso cref="Protocol"/>
        /// <seealso cref="UpnpResponse"/>
        public bool AddPortMapping(int port, Protocol protocol, out UpnpResponse response)
        {
            return this.AddPortMapping(localAddress, port, protocol, out response);
        }

        /// <summary>
        /// Defines a new port mapping on the NAT device, using the specified IP address, port and protocol.
        /// </summary>
        /// <param name="address">A string that contains an IP address in dotted-quad notation for IPv4 and in colon-hexadecimal notation for IPv6. This is the IP address for which to define the port mapping.</param>
        /// <param name="port">The port number for the mapping. It must be a value between 1 and 65535.</param>
        /// <param name="protocol">A member of the <strong>Protocol</strong> enumeration that specifies the Internet protocol to be used for the mapping. It can be TCP or UDP.</param>
        /// <param name="response">An <strong>UpnpResponse</strong> variable that will be filled with information about the response of the operation, if any.</param>
        /// <returns><strong>true</strong> if the port mapping definition succeeds, <strong>false</strong> otherwise.</returns>
        /// <exception cref="System.ArgumentException"><em>port</em> is equal or less than 0.</exception>
        /// <exception cref="System.ArgumentNullException"><em>address</em> is <strong>null</strong>.</exception>
        /// <exception cref="System.FormatException"><em>address</em> is not a valid IP address.</exception>
        /// <exception cref="System.InvalidOperationException">The format of the response message is incorrect.</exception>
        /// <remarks>This method sends an <strong>AddPortMapping</strong> SOAP message to the NAT device. If the methods fails, the <strong>response</strong> object will contain the error code and description.</remarks>
        /// <seealso cref="Protocol"/>
        /// <seealso cref="UpnpResponse"/>
        public bool AddPortMapping(string address, int port, Protocol protocol, out UpnpResponse response)
        {
            return this.AddPortMapping(IPAddress.Parse(address), port, protocol, out response);
        }

        /// <summary>
        /// Defines a new port mapping on the NAT device, using the specified IP address, port and protocol.
        /// </summary>
        /// <param name="address">The IP address for which to define the port mapping.</param>
        /// <param name="port">The port number for the mapping. It must be a value between 1 and 65535.</param>
        /// <param name="protocol">A member of the <strong>Protocol</strong> enumeration that specifies the Internet protocol to be used for the mapping. It can be TCP or UDP.</param>
        /// <param name="response">An <strong>UpnpResponse</strong> variable that will be filled with information about the response of the operation, if any.</param>
        /// <returns><strong>true</strong> if the port mapping definition succeeds, <strong>false</strong> otherwise.</returns>
        /// <exception cref="System.ArgumentException"><em>port</em> is equal or less than 0.</exception>
        /// <exception cref="System.ArgumentNullException"><em>address</em> is <strong>null</strong> or it represents an IP address not valid in this context.</exception>
        /// <exception cref="System.InvalidOperationException">The format of the response message is incorrect.</exception>
        /// <remarks>This method sends an <strong>AddPortMapping</strong> SOAP message to the NAT device. If the methods fails, the <strong>response</strong> object will contain the error code and description.</remarks>
        /// <seealso cref="Protocol"/>
        /// <seealso cref="UpnpResponse"/>
        public bool AddPortMapping(IPAddress address, int port, Protocol protocol, out UpnpResponse response)
        {
            //Richiama il metodo che controlla se gli argomenti sono corretti.
            this.CheckArguments(address, port);

            string description = "UPnP " + port + " " + protocol.ToString();
            string message = string.Format(ADD_PORT_MAPPING_SOAP_MESSAGE, base.ServiceType, address, port, protocol.ToString(), description);
            string header = string.Format(HEADER1, base.ControlUrl, base.ServiceType, Operation.AddPortMapping.ToString(), base.EndPoint.Address, base.EndPoint.Port, UTF8Encoding.ASCII.GetBytes(message).Length);

            return this.SendRequest(Operation.AddPortMapping, header, message, out response);
        }

        #endregion

        #region GetExternalIPAddress()

        /// <summary>
        /// Retrieves the public IP address of the NAT device, i.e. the address with which the device is publicly visible on the Internet. 
        /// </summary>
        /// <param name="response">An <strong>UpnpResponse</strong> variable that will be filled with information about the response of the operation, if any.</param>
        /// <exception cref="System.InvalidOperationException">The format of the response message is incorrect.</exception>
        /// <returns>The IP address of the NAT device, if the method succeeds, <strong>null</strong> otherwise.</returns>
        /// <remarks>This method sends a <strong>GetExternalIPAddress</strong> SOAP message to the NAT device. If the methods fails, the <strong>response</strong> object will contain the error code and description.</remarks>
        /// <seealso cref="UpnpResponse"/>
        public IPAddress GetPublicIPAddress(out UpnpResponse response)
        {
            IPAddress ipAddress = null;
            string message = string.Format(GET_EXTERNAL_IP_ADDRESS_SOAP_MESSAGE, base.ServiceType);
            string header = string.Format(HEADER1, base.ControlUrl, base.ServiceType, Operation.GetExternalIPAddress.ToString(), base.EndPoint.Address, base.EndPoint.Port, UTF8Encoding.ASCII.GetBytes(message).Length);

            bool ret = this.SendRequest(Operation.GetExternalIPAddress, header, message, out response);
            if (ret)
                ipAddress = IPAddress.Parse(response.Value);

            return ipAddress;
        }

        #endregion

        #region DeletePortMapping

        /// <summary>
        /// Deletes an existing port mapping on the NAT device.
        /// </summary>
        /// <param name="port">The port number of the mapping to be removed.</param>
        /// <param name="protocol">A member of the <strong>Protocol</strong> enumeration that specifies the Internet protocol of the mapping to be removed. It can be TCP or UDP.</param>
        /// <param name="response">An <strong>UpnpResponse</strong> variable that will be filled with information about the response of the operation, if any.</param>
        /// <returns><strong>true</strong> if the port mapping removal succeed, <strong>false</strong> otherwise.</returns>
        /// <exception cref="System.ArgumentException"><em>port</em> is equal or less than 0.</exception>
        /// <exception cref="System.InvalidOperationException">The format of the response message is incorrect.</exception>
        /// <remarks>This method sends a <strong>DeletePortMapping</strong> SOAP message to the NAT device. If the methods fails, the <strong>response</strong> object will contain the error code and description.</remarks>
        /// <seealso cref="Protocol"/>
        /// <seealso cref="UpnpResponse"/>
        public bool DeletePortMapping(int port, Protocol protocol, out UpnpResponse response)
        {
            //Richiama il metodo che controlla se gli argomenti sono corretti.
            this.CheckArguments(localAddress, port);

            string message = string.Format(REMOVE_PORT_MAPPING_SOAP_MESSAGE, base.ServiceType, port, protocol.ToString());
            string header = string.Format(HEADER1, base.ControlUrl, base.ServiceType, Operation.DeletePortMapping.ToString(), base.EndPoint.Address, base.EndPoint.Port, UTF8Encoding.ASCII.GetBytes(message).Length);

            return this.SendRequest(Operation.DeletePortMapping, header, message, out response);
        }

        #endregion

        //public void GetPortMapping()
        //{
        //    string message = string.Format(GET_PORT_MAPPING_SOAP_MESSAGE, base.ServiceType);
        //    string header = string.Format(HEADER1, base.ControlUrl, base.ServiceType, Operation.GetGenericPortMappingEntry.ToString(), base.EndPoint.Address, base.EndPoint.Port, UTF8Encoding.ASCII.GetBytes(message).Length);

        //    string s;
        //    this.SendRequest(Operation.GetGenericPortMappingEntry.ToString(), header, message, out s);
        //}

        private void CheckArguments(IPAddress address, int port)
        {   
            if (address == null || address == IPAddress.None || address == IPAddress.Loopback || address == IPAddress.Any || address == IPAddress.Broadcast || address == IPAddress.IPv6Any || address == IPAddress.IPv6Loopback || address == IPAddress.IPv6None)
                throw new ArgumentException("The IP Address isn't valid");
            if (port <= 0)
                throw new ArgumentException("The port number must be grater than 0");
        }

        private bool SendRequest(Operation operation, string header, string message, out UpnpResponse response)
        {
            bool successful = false;
            string method = operation.ToString();

            response = new UpnpResponse();

            //Si connette alla periferica NAT.
            using (TcpClient client = new TcpClient())
            {
                byte[] bufferToSend = UTF8Encoding.ASCII.GetBytes(header + message);

                client.Connect(base.EndPoint);
                using (NetworkStream stream = client.GetStream())
                {
                    //Invia la richiesta di apertura della porta.
                    stream.Write(bufferToSend, 0, bufferToSend.Length);

                    StringBuilder responseBuffer = new StringBuilder();
                    int contentLength = 0;

                    //Attende la risposta.
                    byte[] buffer = new byte[1024];
                    int recv = stream.Read(buffer, 0, buffer.Length);
                    if (recv > 0)
                    {
                        responseBuffer.Append(UTF8Encoding.ASCII.GetString(buffer, 0, recv));

                        string[] lines = responseBuffer.ToString().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                        foreach (string line in lines)
                        {
                            if (line.ToLower().StartsWith("content-length"))
                            {
                                //Estrae la lunghezza del messaggio.
                                contentLength = int.Parse(line.Substring(line.IndexOf(':') + 1));
                                break;
                            }
                        }
                    }

                    //Cicla finché sono disponibili ulteriori dati.
                    Thread.Sleep(500);
                    if (contentLength > 0 && stream.DataAvailable)
                        responseBuffer.Length = 0;
                    while (contentLength > 0 && stream.DataAvailable)
                    {
                        Array.Clear(buffer, 0, buffer.Length);
                        recv = stream.Read(buffer, 0, buffer.Length);
                        if (recv > 0)
                        {
                            responseBuffer.Append(UTF8Encoding.ASCII.GetString(buffer, 0, recv));
                            contentLength -= recv;
                        }
                    }

                    //Carica la risposta in un XmlDocument per estrarne le informazioni.
                    XmlDocument doc = new XmlDocument();

                    //Controlla se la risposta contiene l'instestazione XML, poiché alcuni router non la inseriscono.
                    string temp = responseBuffer.ToString();
                    int pos = temp.IndexOf("<?xml");
                    if (pos < 0)
                    {
                        //La risposta non contiene l'instestazione XML.
                        pos = temp.IndexOf("<s:Envelope");
                        if (pos < 0)
                            throw new InvalidOperationException("The format of the response message is incorrect");
                    }

                    string xml = temp.Substring(pos);
                    doc.LoadXml(xml);
                    //Create an XmlNamespaceManager for resolving namespaces.
                    XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
                    nsmgr.AddNamespace("u", base.ServiceType);

                    //Controlla l'esito della richiesta.
                    XmlNode node = doc.SelectSingleNode("descendant::u:" + method + "Response", nsmgr);
                    if (node != null)
                    {
                        //Se node.InnerText è vuoto e si sta ricercando l'indirizzo IP pubblico del router, significa che il router non è connesso a Internet.
                        if (operation == Operation.GetExternalIPAddress && string.IsNullOrEmpty(node.InnerText))
                        {
                            response.Value = null;
                            response.Error = new UpnpException(-1, "Not connected");
                            successful = false;
                        }
                        else
                        {
                            //La richiesta ha avuto esito positivo.
                            response.Value = node.InnerText;
                            response.Error = null;
                            successful = true;
                        }
                    }
                    else
                    {
                        //La richiesta ha avuto esito negativo. Estrae le informazioni di errore.
                        nsmgr.AddNamespace("u", ERROR_NAMESPACE);
                        node = doc.SelectSingleNode("descendant::u:UPnPError", nsmgr);
                        int errorCode = int.Parse(node.SelectSingleNode("u:errorCode", nsmgr).InnerText);
                        string errorDescription = node.SelectSingleNode("u:errorDescription", nsmgr).InnerText;
                        response.Value = null;
                        response.Error = new UpnpException(errorCode, errorDescription);
                    }
                }
            }

            return successful;
        }

    }
}